﻿using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Text;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace CodeAnalysisRuleCollection.Enumerables.CSharp
{
    /// <example>source.Count() == 0</example>
    [DiagnosticAnalyzer]
    [ExportDiagnosticAnalyzer(DiagnosticId, LanguageNames.CSharp)]
    internal class CountEqualsZeroAnalyzer : ISyntaxNodeAnalyzer<SyntaxKind>
    {
        internal const string DiagnosticId = "CountEqualsZero";
        internal const string Description = "Possible enumeration of the entire sequence";
        internal const string MessageFormat = "Use Any() instead of Count() to avoid possible enumeration of the entire sequence.";
        internal const string Category = "Performance";

        private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(DiagnosticId, Description, MessageFormat, Category, DiagnosticSeverity.Warning);

        public ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
        {
            get
            {
                return ImmutableArray.Create(Rule);
            }
        }

        public ImmutableArray<SyntaxKind> SyntaxKindsOfInterest
        {
            get
            {
                return ImmutableArray.Create(SyntaxKind.EqualsExpression);
            }
        }

        public void AnalyzeNode(SyntaxNode node, SemanticModel semanticModel, Action<Diagnostic> addDiagnostic, CancellationToken cancellationToken)
        {
            var binaryExpression = (BinaryExpressionSyntax)node;
            var left = binaryExpression.Left;
            var right = binaryExpression.Right;

            if (
                IsCallToEnumerableCount(semanticModel, left, cancellationToken) &&
                IsZeroConstantValue(semanticModel, right, cancellationToken)
                ||
                IsCallToEnumerableCount(semanticModel, right, cancellationToken) &&
                IsZeroConstantValue(semanticModel, left, cancellationToken)
            )
                addDiagnostic(Diagnostic.Create(Rule, node.GetLocation()));
        }

        private bool IsCallToEnumerableCount(SemanticModel semanticModel, ExpressionSyntax expression, CancellationToken cancellationToken)
        {
            var invocation = expression as InvocationExpressionSyntax;
            if (invocation == null)
                return false;

            var call = invocation.Expression as MemberAccessExpressionSyntax;
            if (call == null)
                return false;

            var methodSymbol = semanticModel.GetSymbolInfo(call, cancellationToken).Symbol as IMethodSymbol;
            if (methodSymbol == null ||
                methodSymbol.Name != "Count" ||
                methodSymbol.ConstructedFrom == null)
                return false;

            var enumerable = semanticModel.Compilation.GetTypeByMetadataName(typeof(Enumerable).FullName);
            if (enumerable == null || !methodSymbol.ConstructedFrom.ContainingType.Equals(enumerable))
                return false;

            return true;
        }

        private bool IsZeroConstantValue(SemanticModel semanticModel, ExpressionSyntax expression, CancellationToken cancellationToken)
        {
            var constantValue = semanticModel.GetConstantValue(expression, cancellationToken);

            return constantValue.HasValue && constantValue.Value.Equals(0);
        }
    }


    [ExportCodeFixProvider(CountEqualsZeroAnalyzer.DiagnosticId, LanguageNames.CSharp)]
    internal class CountEqualsCodeFixProvider : ICodeFixProvider
    {
        public IEnumerable<string> GetFixableDiagnosticIds()
        {
            yield return CountEqualsZeroAnalyzer.DiagnosticId;
        }

        public async Task<IEnumerable<CodeAction>> GetFixesAsync(Document document, TextSpan span, IEnumerable<Diagnostic> diagnostics, CancellationToken cancellationToken)
        {
            var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
            var diagnosticSpan = diagnostics.First().Location.SourceSpan;

            // find binary expression
            var expression = root.FindToken(diagnosticSpan.Start).Parent.AncestorsAndSelf().OfType<BinaryExpressionSyntax>().First();

            return new[] { CodeAction.Create("Change use of Enumerable.Count() to Enumerable.Any()", c => ChangeToAnyAsync(root, document, expression, c)) };
        }

        private async Task<Document> ChangeToAnyAsync(SyntaxNode root, Document document, BinaryExpressionSyntax expression, CancellationToken cancellationToken)
        {
            // find invocation expression
            var invocation = expression.DescendantNodes().OfType<InvocationExpressionSyntax>().First();

            // replace node
            var newRoot = root.ReplaceNode<SyntaxNode, ExpressionSyntax>(expression,
                SyntaxFactory.PrefixUnaryExpression(SyntaxKind.LogicalNotExpression,
                    invocation
                        .WithExpression(
                            (invocation.Expression as MemberAccessExpressionSyntax).WithName(SyntaxFactory.IdentifierName("Any"))
                        )
                        .WithAdditionalAnnotations(Formatter.Annotation)
                )
            );

            return document.WithSyntaxRoot(newRoot);
        }
    }
}
